<?php

require_once './log4php/Logger.php';
require_once './facebook-php-sdk/facebook.php';
require_once './config.php';
require_once './Utils.php';
require_once './Error.php';

Logger::configure('./log4j.properties');

/**
 * The FBConnect class is responsible for the connection/communication with the FB server(s).
 * <br/>Extends the FB API library's Facebook class.
 */
class FBConnect extends Facebook {

    /**
     *
     * @var Logger the logger
     */
    private $log;

    /**
     *
     */
    public function __construct() {
        // use '&' as arg separator instead of '&amp;'
        ini_set('arg_separator.output', '&');
        
        // init logging
        $this->log = Logger::getLogger(__CLASS__);
        $this->log->debug("[START] " . __METHOD__);

        // init parent contructor
        parent::__construct(array(
                    'appId' => APP_ID,
                    'secret' => APP_SECRET,
                    'cookie' => TRUE,
                    'fileUpload' => TRUE    // use POST data
                ));


        // convert the old-form cookies into the expected form
        $this->convertOldAuthToNew();

        $this->log->debug("UID: " . $this->getUser());
        $this->log->debug("Session: " . Utils::toString($this->getSession()));
        $this->log->debug("[ END ] " . __METHOD__);
    }

    /**
     * Makes a set of FQL queries in parallel.  This method takes a dictionary
     * of FQL queries where the keys are names for the queries.  Results from
     * one query can be used within another query to fetch additional data.
     *
     * @see http://developers.facebook.com/docs/reference/rest/fql.multiquery
     *
     * @param string $queries JSON-encoded dictionary of queries to evaluate
     *
     * @return array generalized array representing the results
     */
    public function doMultiquery($queries) {
        $this->log->debug('[START] ' . __METHOD__);
        $this->log->debug('Queries: ' . $queries);

        $start = microtime(TRUE);
        $result = $this->callRestAPI('fql.multiquery',
                array('queries' => $queries));
        $end = microtime(TRUE);

        $this->log->debug('Execution took ' . ($end - $start) . 's');
        $this->log->debug('[ END ] ' . __METHOD__ . ': ' . Utils::toString($result));

        return $result;
    }

    /**
     * Converts the old-form cookies into the expected form.
     *
     * INPUT:
     * cf77865a5070f2c2ba3b52cbf3371579_expires=1284375600;
     * cf77865a5070f2c2ba3b52cbf3371579_session_key=2.lZ_sr8pqAZcc_kp2oEmyVQ__.3600.1284375600-613374451;
     * cf77865a5070f2c2ba3b52cbf3371579_user=613374451;
     * cf77865a5070f2c2ba3b52cbf3371579_ss=y3dHI5VJo_KTUA4u8PvVBg__;
     * cf77865a5070f2c2ba3b52cbf3371579=fc6af233e4d14837de64444f30bccbeb
     *
     * OUTPUT:
     * 'fbs_286811277465' = '"
     *      session_key=2.VqDNiMGNCLm3XGJOw_5J4w__.3600.1284372000-1014450671&
     *      uid=1014450671&
     *      expires=1284372000&
     *      secret=fC9vB6AjUjzqZIdiTUIHWg__&
     *      access_token=286811277465%7C2.VqDNiMGNCLm3XGJOw_5J4w__.3600.1284372000-1014450671%7CfW7FmYmLDF9C2sAvA_6dZ-AlzM0&
     *      sig=604ee734dccbca9b085d9d6c5281d449"'
     *
     * So from the input-like cookie string makes the output-like cookie string.
     * The not related cookie values are leaved untouched, so they appear also in the output string.
     * The access_token is got from the FB server separatly according to the session_key.
     *
     * @throws Exception ERROR_SESSION_INVALID_OR_MISSING if can't get OAuth access_token for the session key.
     */
    private function convertOldAuthToNew() {
        $this->log->debug('[START] ' . __METHOD__);

        $cookieName = $this->getSessionCookieName();
        // check if it is available any of the expected session definition:
        // - query param 'signed_request' (when in FB canvas)
        // - query param 'session'
        // - cookie 'fbs_<app_id>'
        if (isset($_REQUEST['signed_request'])
                || isset ($_REQUEST['session'])
                || ($this->useCookieSupport() && isset ($_COOKIE[$cookieName]))) {
            // expected session available => don't do any change
            $this->log->debug("Expected session definition found...skip");
            $this->log->debug('[ END ] ' . __METHOD__);
            return;
        }

        $this->log->debug("Trying to find old-form session cookies in " . Utils::toString($_COOKIE));
        
        $sessionData = array();
        foreach ($_COOKIE as $key => $value) {
            if (strpos($key, API_KEY) === 0) {  // if starts with <API_KEY>
                unset($_COOKIE[$key]);          // remove from the cookies

                if (strcmp(API_KEY . '_user', $key) == 0) {
                    $sessionData['uid'] = $value;
                }
                else if (strcmp(API_KEY . '_session_key', $key) == 0) {
                    $sessionData['session_key'] = $value;
                }
                else if (strcmp(API_KEY . '_expires', $key) == 0) {
                    $sessionData['expires'] = $value;
                }
                else if (strcmp(API_KEY . '_ss', $key) == 0) {
                    $sessionData['secret'] = $value;
                }
            }
        }

        // if found all the 4 required session data
        if (count($sessionData) == 4) {
            $access_token = $this->getOAuthFromSession($sessionData['session_key']);

            if (!$access_token) {
                throw new Exception("Can't get OAuth access_token for sessin key!", ERROR_SESSION_INVALID_OR_MISSING);
            }

            $sessionData['access_token'] = $access_token;
            $sessionData['sig'] = $this->generateSignature($sessionData, $this->getApiSecret());
            // append the built up session string
            $_COOKIE[$cookieName] = '"' . http_build_query($sessionData) . '"';
        }
        else if (count($sessionData) == 0) {
            // not found any part of old-form session cookie
            $this->log->debug("Not found old-like session cookies!");
        }
        
        $this->log->debug("Output: " . Utils::toString($_COOKIE));
        $this->log->debug('[ END ] ' . __METHOD__);
    }

    /**
     * Calls the REST API.
     *
     * @param string $method the name of the API method to be called
     * @param array $params parameters for the API method
     *
     * @throws FacebookApiException in case of any FB API problem
     */
    private function callRestAPI($method, $params) {
        $this->log->debug("[START] " . __METHOD__ . ": " . Utils::toString(func_get_args()));

        // build up the param array for the REST API call
        $args = array_merge(
                        array('method' => $method),
                        $params != NULL ? $params : array());

        // call the API
        try {
            $result = $this->api($args);
            $this->log->debug("api() OK response = " . Utils::toString($result));
            $this->log->debug("[ END ] " . __METHOD__);

            return $result;
        } catch (FacebookApiException $e) {
            $this->log->error("API call result: " . Utils::toString($e->getResult()));
            $this->log->error("Exception occured at " . $e->getFile() . ":" . $e->getLine() . " Code: " . $e->getCode() . ": " . $e->getMessage());
            $this->log->error("Exception trace: " . $e->getTraceAsString());
            $this->log->debug("[ END ] " . __METHOD__);
            throw $e;   // propagate the exception
        }
    }

    /**
     * Builds up the application access_token.
     *
     * @return string application access_token
     */
    private function getAppAccessToken() {
        return $this->getAppId() . '|' . $this->getApiSecret();
    }

//    /**
//     * Executes the 'data.getAssociatedObjects' API call.<br/>
//     * Retrieve a list of associated objects.
//     *
//     * @param string $name name of association
//     * @param string $obj_id who's association to retrieve
//     * @param boolean $no_data only return object ids
//     * @return array associated objects
//     *
//     * @throws FacebookApiException in case of any FB API problem
//     */
//    public function data_getAssociatedObjects($name, $obj_id, $no_data = true) {
//        return $this->callRestAPI('data.getAssociatedObjects',
//                array('name' => $name,
//                    'obj_id' => $obj_id,
//                    'no_data' => $no_data));
//    }

//    /**
//     * Executes the 'data.updateObject' API call.<br/>
//     * Update an existing object.
//     *
//     * @param string $obj_id object's id
//     * @param array $properties new properties
//     * @param boolean $replace true for replacing existing properties, false for merging
//     * @return boolean true in case of success???
//     */
//    public function data_updateObject($obj_id, $properties, $replace = false) {
//        return $this->doFQLAction('data.updateObject',
//                array('obj_id' => $obj_id,
//                    'properties' => json_encode($properties),
//                    'replace' => $replace,
//                    'access_token' => $this->getAppAccessToken()));
//    }

//    /**
//     * Executes the 'data.removeAssociation' API call.<br/>
//     * Remove an association between two objects.
//     *
//     * @param string $name name of association
//     * @param string $obj_id1 id of first object
//     * @param string $obj_id2 id of second object
//     * @return boolean true in case of success???
//     */
//    public function data_removeAssociation($name, $obj_id1, $obj_id2) {
//        return $this->doFQLAction('data.removeAssociation',
//                array('name' => $name,
//                    'obj_id1' => $obj_id1,
//                    'obj_id2' => $obj_id2,
//                    'access_token' => $this->getAppAccessToken()));
//    }

//    /**
//     * Executes the 'data.setAssociation' API call.<br/>
//     * Create or modify an association between two objects.
//     *
//     * @param string $name name of association
//     * @param string $obj_id1 id of first object
//     * @param string $obj_id2 id of second object
//     * @param string $data extra string data to store
//     * @param string $assoc_time extra time data; default to creation time
//     * @return boolean true in case of success???
//     */
//    public function data_setAssociation($name, $obj_id1, $obj_id2, $data = null, $assoc_time = null) {
//        return $this->doFQLAction('data.setAssociation',
//                array('name' => $name,
//                    'obj_id1' => $obj_id1,
//                    'obj_id2' => $obj_id2,
//                    'data' => $data,
//                    'assoc_time' => $assoc_time,
//                    'access_token' => $this->getAppAccessToken()));
//    }

//    /**
//     * Executes the 'data.createObject' API call.<br/>
//     * Create a new object.
//     *
//     * @param string $obj_type object type's name
//     * @param array $properties properties to set initially
//     * @return string newly created object's id
//     */
//    public function data_createObject($obj_type, $properties = null) {
//        return $this->doFQLAction('data.createObject',
//                array('obj_type' => $obj_type,
//                    'properties' => json_encode($properties),
//                    'access_token' => $this->getAppAccessToken()));
//    }

//    /**
//     * TODO: not tested yet!!!!
//     *
//     * @param <type> $obj_id
//     * @return <type>
//     */
//    public function data_deleteObject($obj_id) {
//        return $this->doFQLAction('data.deleteObject',
//                array('obj_id' => $obj_id,
//                    'access_token' => $this->getAppAccessToken()));
//    }

//    /**
//     * Executes the $method API call.<br/>
//     * Makes a Data Store API request (not only FQL query).
//     *
//     * @param string $method name of the API method
//     * @param array $params parameter of the call
//     * @return mixed the Data Store API call result
//     */
//    private function doFQLAction($method, $params) {
//        $this->log->debug("[START] " . __METHOD__ . ": " . Utils::toString(func_get_args()));
//
//        $args = array(
//            'api_key' => $this->getAppId(),
//            'format' => 'json-strings',
//            'method' => $method,
//            'access_token' => $this->getAppAccessToken()
//        );
//
//        if (is_array($params)) {
//            $args = array_merge($args, $params);
//        }
//
//        $result = $this->_oauthRequest($this->getApiUrl($method), $args);
//        $result = trim($result, '"');   // the response is enclosed by '"' - trim them
//
//        // check whether the returned result is an error message
//        if (is_array($result) && isset($result['error_code'])) {
//            $this->log->error("Result: " . Utils::toString($result));
//            $this->log->debug("[END] " . __METHOD__);
//            throw new FacebookApiException($result);
//        }
//
//        $this->log->debug("Result: " . Utils::toString($result));
//        $this->log->debug("[END] " . __METHOD__);
//        return $result;
//    }


    /**
     * Executes the 'admin.getAppProperties' API call.<br/>
     * Get the properties that you have set for an app.
     *
     * @param array $properties List of properties names to fetch
     * 
     * @return array A map from property name to value
     *
     * @throws FacebookApiException in case of any FB API problem
     */
    public function admin_getAppProperties($properties) {
        $this->log->debug("[START] " . __METHOD__ . ": " . Utils::toString(func_get_args()));

        $args = array(
            'api_key' => $this->getAppId(),
            'format' => 'json-strings',
            'method' => 'admin.getAppProperties',
            'access_token' => $this->getAppAccessToken(),
            'properties' => json_encode($properties)
        );

        $result = $this->_oauthRequest($this->getApiUrl('admin.getAppProperties'), $args);
        $result = trim($result, '"');

        // results are returned, errors are thrown
        if (is_array($result) && isset($result['error_code'])) {
            $this->log->error("Result: " . Utils::toString($result));
            throw new FacebookApiException($result);
        }

        $result = stripcslashes(stripcslashes($result));
        $result = json_decode($result, TRUE);

        $this->log->debug("[END] " . __METHOD__ . ": " . Utils::toString($result));
        return $result;
    }

    /**
     * Executes the 'stream.publish' API call.<br/>
     * Publish a post to the user's stream.
     *
     * @param <type> $message
     * @param <type> $attachment
     * @param <type> $action_links
     * @param <type> $target_id
     * @param <type> $uid
     * @return <type>
     */
    public function stream_publish($message, $attachment = null,
            $action_links = null, $target_id = null, $uid = null) {
        return $this->callRestAPI('stream.publish',
                array('message' => $message,
                    'attachment' => $attachment,
                    'action_links' => $action_links,
//                    'target_id' => $target_id,
                    'uid' => ($uid ? $uid : $this->getUser())));
    }

    /**
     *
     * Tries to get the OAuth access token for the given session key.
     *
     * @param string $session_key session key
     * @return string access token or NULL in case of any problem
     */
    public function getOAuthFromSession($session_key) {
        $this->log->debug("[START] " . __METHOD__ . ": " . Utils::toString(func_get_args()));
        
        $ch = curl_init('https://graph.facebook.com/oauth/exchange_sessions');

        $opts = array(
            CURLOPT_POST => TRUE,
            CURLOPT_POSTFIELDS => array(
                'client_id' => APP_ID,
                'client_secret' => APP_SECRET,
                'sessions' => $session_key
            ),
            CURLOPT_RETURNTRANSFER => TRUE,
            CURLOPT_SSL_VERIFYPEER => FALSE
        );
        curl_setopt_array($ch, $opts);

        $response = curl_exec($ch);
        $this->log->info("CURL response: " . $response);
        
        $result = NULL;
        // check the the response
        if (!$response) {   // error happened / wrong response
            $this->log->error('ERROR_CODE: ' . curl_errno($ch));
            $this->log->error('ERROR: ' . curl_error($ch));
            $result = NULL;
        }
        else {
            $response = json_decode($response, TRUE);
            $result = $response[0]['access_token'];
        }

        // close the connection
        curl_close($ch);

        $this->log->debug("[ END ] " . __METHOD__ . ": " . $result);
        return $result;
    }

}

?>
